
import dep_graph.keys
import logging

logger = logging.getLogger(__name__)

class Error(Exception):
  """cache_view exception base class."""

class CircularDependencyError(Error):
  """A fileset depends on its self."""
  def __init__(self, fileset):
    super().__init__('Circular dependency detected')
    self.fileset = fileset

class DoubleDependencyError(Error):
  """A fileset depends on another fileset twice."""
  def __init__(self, fileset):
    super().__init__('Dependency listed twice')
    self.fileset = fileset

class GenRule(object):
  cmd = ""
  def __init__(self, graph, key):
    self.__key = key
    self.__graph = graph
    self.out = FileBucket(graph, key, "out")
    self.inp = FileBucket(graph, key, "inp")
    self.meta = FileBucket(graph, key, "meta")
    self.wipe = FileBucket(graph, key, "wipe")
    self.valid = False
    self.env = None

  def rulename(self):
    """ string rule name containing BUILD file."""
    return self.__key.rulename

  def dirname(self):
    """ string directory name containing BUILD file."""
    return self.__key.dirname

  @property
  def key(self):
    """ dep_graph.keys.GenRuleKey
    key that can be used to fetch this fset from the graph. """
    return self.__key

  def full_name(self):
    """ string full name """
    return self.__key.full_name()

  def remove_from_graph(self):
    """Removes self from the graph."""
    self.__graph.remove(self.__key)
    for bucket in (self.out, self.inp, self.meta, self.wipe):
      for key in bucket.getFsetKeys():
        self.__graph.delete_fset_with_key(key)

  def __str__(self):
    return self.__key.full_name()

  def __repr__(self):
    return self.__key.full_name()

  def set_cmd(self, cmd):
    """ cmd is a list of strings to pass to execv """
    self.cmd = cmd
    self.__graph.write_gen_rule(self.__key)

  def get_cmd(self):
    """ returns list of strings to pass to execv """
    return self.cmd

  def set_env(self, env):
    """ env is a dictionary to use as the environment."""
    self.env = env
    self.__graph.write_gen_rule(self.__key)

  def get_env(self):
    """ returns dictionary of strings to pass to execv """
    return self.env

  def deep_inspect(self):
    """ Debug string that doesn't follow to any other rules."""
    return (str(self) + ".out { " + str(self.out) 
        + "} .inp { " + str(self.inp) + " }" + repr(self.valid))

  def set_valid(self, valid = True):
    """ sets bool if node is valid (cmd already run) or not."""
    self.valid = valid
    self.__graph.write_gen_rule(self.__key)

  def get_valid(self):
    """ bool if node is valid (cmd already run) or not."""
    return self.valid

  def reverse_dep_keys(self):
    """ lookups up reverse transitive closure of dep_graph.keys. """
    for bucket in (self.out, self.inp, self.meta, self.wipe):
      for fset in bucket.getFsets():
        for key in fset.reverse_dep_keys():
          yield key.genrule_key


class FileBucket(object):
  """ FileBucket for storing sets of files """
  def __init__(self, graph, key, name):
    self.__name = name
    self.__graph = graph
    self.__key = key
    self.__keys = set()

  def __getattr__(self, name):
    """ overrides getattr to make a fileset for each string property."""
    if name in self.__dict__:
      return self.__dict__[name]
    key = dep_graph.keys.FileSetKey(self.__key, self.__name, name)
    val = self.__graph.get_fset_with_key(key)
    self.__dict__[name] = val 
    if name not in self.__keys:
      self.__keys.add(name) 
      self.__graph.write_gen_rule(self.__key)
    return val 

  def getKeys(self):
    """ get list of strings in this bucket (like cc_hdrs etc.)."""
    return self.__keys

  def getFsets(self):
    """ iterator of dep_graph.cache_view.FileSet for this file bucket."""
    for name in self.__keys:
      yield getattr(self, name)

  def getFsetKeys(self):
    """ iterator of dep_graph.keys.FileSetKeys for this file bucket."""
    for name in self.__keys:
      yield dep_graph.keys.FileSetKey(self.__key, self.__name, name)

  """ Pickling:"""
  def __getstate__(self):
    """ returns tuple of pickleable state."""
    return (self.__keys, self.__key, self.__graph, self.__name)
  def __setstate__(self, state):
    """ takes tuple of pickleable state. """
    self.__keys, self.__key, self.__graph, self.__name = state
  
  def __getnewargs__(self):
    """ returns constructor to use in pickling / unpickling."""
    return (FileBucket,)

  def __str__(self):
    """ """
    return repr(self.__keys)

  def __contains__(self, key):
    """ key : dep_graph.keys.FileSetKey checks if is a child fset."""
    return key in self.__keys
    
class FileSet(object):
  """Represents a DAG of strings.
  
  Attributes:
    __graph: The dep_graph.cache.FileBackedCachedGraph this FileSet belongs to.
    __key: The corresponding dep_graph.keys.FileSetKey.
    files: The set of files in this FileSet.
    __dependencies: A list of the keys of other FileSets we depend on.
    __reverse_dependencies: A list of the keys of other FileSets that depend on
                            us.
  """
  def __init__(self, graph, key):
    """Args:
      graph: The FileBackedCachedGraph this FileSet belongs to.
      key: The FileSetKey corresponding to self.
    """
    self.__graph = graph
    self.__key = key
    self.files = set()
    self.__dependencies = []
    self.__reverse_dependencies = set()

  @property
  def gen_rule(self):
    """Returns the dep_graph.cache_view.GenRule corresponding to self."""
    return self.__graph.get_gen_rule_with_key(self.__key.genrule_key)

  @property
  def key(self):
    """ dep_graph.keys.FileSetKey
    key that can be used to fetch this fset from the graph. """
    return self.__key

  def add(self, f):
    """Adds a string to the set of files included in this fileset.
    
    Args:
      f: The str to add.
    """
    if not isinstance(f, str):
      raise TypeError(repr(f) + " is not str")
    try:
      self.files.add(f)
      self.__graph.link_file_to_fset(f, self.__key)
      self.__graph.write_fset(self.__key)
    except:
      try:
        self.files.remove(f)
      except KeyError:
        pass
      raise

  def add_cmd(self, cmd):
    """Adds a command and list of args to this fileset.

    Args:
      cmd: List of strings, The command and args.
    """
    try:
      self.files.add(cmd)
      self.__graph.write_fset(self.__key)
    except:
      try:
        self.files.remove(cmd)
      except KeyError:
        pass
      raise

  def add_all(self, files):
    """Adds multiple strings to the set of files included in this fileset.

    Args:
      files: A sequence of files to add.
    """
    for f in files:
      if not isinstance(f, str):
        raise TypeError(repr(f) + " is not str")
      if f not in self.files:
        self.__graph.link_file_to_fset(f, self.__key)
    self.files.update(files)
    self.__graph.write_fset(self.__key)

  def depends_on(self, other):
    """Adds another FileSet that this one depends on.
    
    Args:
      other: Another FileSet to depend on.
    """
    if other is self:
      raise CircularDependencyError(self)
    if self.__key in other.__reverse_dependencies:
      raise DoubleDependencyError(self)
    self.__dependencies.append(other.key)
    other.__reverse_dependencies.add(self.__key)
    self.__graph.write_fset(other.key)
    self.__graph.write_fset(self.__key)

  def remove_from_graph(self):
    """Removes self from the graph."""
    self.__graph.remove(self.__key)
    for key in self.__dependencies:
      ofset = self.__graph.get_existing_fset_with_key(key)
      assert self.__key in ofset.__reverse_dependencies
      ofset.__reverse_dependencies.remove(self.__key)
      ofset.__graph.write_fset(ofset.__key)
    for key in self.__reverse_dependencies:
      ofset = self.__graph.get_existing_fset_with_key(key)
      assert self.__key in ofset.__dependencies
      ofset.__dependencies.remove(self.__key)
      ofset.__graph.write_fset(ofset.__key)
    self.__graph.try_delete_gen_rule_with_key(self.key.genrule_key)

    for fname in self.files:
      if type(fname) is str:
        self.__graph.unlink_file_from_fset(fname, self.__key)

  @property
  def dependencies(self):
    """ returns: Iterator of immediate fset dependencies."""
    for key in self.__dependencies:
      fset = self.__graph.get_existing_fset_with_key(key)
      yield fset

  @property
  def reverse_dependencies(self):
    """ returns: Iterator of immediate fset children."""
    for key in self.__reverse_dependencies:
      fset = self.__graph.get_existing_fset_with_key(key)
      yield fset

  def reverse_dep_keys(self):
    return iter(self.__reverse_dependencies)

  def is_output(self):
    """Returns True if this is an output file set."""
    return self.__key.bucket_name == 'out'

  def is_input(self):
    """Returns True if this is an input file set."""
    return self.__key.bucket_name == 'inp'

  def is_wipe(self):
    """Returns True if this is an wiping file set."""
    return self.__key.bucket_name == 'wipe'

  def depth_first_search(self, output, visited):
    """Performs a depth-first iteration through self.

    Args:
      output: A sequence to append the result to.
      visited: The sequence of FileSets that have already been visited.

    returns: None
    """
    if self.__key in visited: return
    visited.add(self.__key)
    for dep in self.dependencies:
      dep.depth_first_search(output, visited)
    output.append(self)

  def __iter__(self):
    assert False

  def file_names(self):
    """Yields all of the filenames contained in self recursively in reverse
    depth-first order."""
    depth_first = []
    self.depth_first_search(depth_first, set())
    for f_set in reversed(depth_first):
      for f in f_set.files:
        yield(f)

  def __str__(self):
    """ returns: string  // .file_names() joined together."""
    return " ".join(self.file_names())

  def deep_inspect(self):
    """ Debug string showing surface level pointers to dependencies and files."""
    return ("dependencies " + repr(self.__dependencies) +
            "; files: " + repr(self.files))
